liquid_xlsx 0.1.0 → 0.2.1
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 +32 -0
- data/README.md +24 -1
- data/lib/liquid_xlsx/package.rb +23 -1
- data/lib/liquid_xlsx/renderer.rb +5 -0
- data/lib/liquid_xlsx/version.rb +1 -1
- data/lib/liquid_xlsx/worksheet.rb +22 -0
- data/lib/liquid_xlsx.rb +6 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2962349e5fe1197bffc9c0d28d21be73e83b97f71cf3e8ce86579c07246e5072
|
|
4
|
+
data.tar.gz: 67d36e10dfaf54790ffd949e175ba80b24940d331eb9e9b826661158b32c08ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f9379732f56d318bb9dba0a0fbf5a9c82e2ec782a97109043eaf6e5272aeb4a2a5eb3b7fa592dbab1d059543ba5dc3dbf3f9fdbc1637bf7c9df42ecacec88ce
|
|
7
|
+
data.tar.gz: '080cdb56014357ece810afd62e5e42271f7eb403cdcd62cb63697f894b4814b67b6c2959f11dd911dc98099b5be18e715b0cdc1c27b16cbe95c3b92e6862450f'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1 (2026-08-09)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Rendered workbooks are no longer marked ZIP64. Members were streamed without
|
|
8
|
+
a declared size, and rubyzip 3 — where ZIP64 is on by default — then flagged
|
|
9
|
+
every member as ZIP64: version 4.5 in the local header and `0xFFFFFFFF`
|
|
10
|
+
sizes. Such an archive still satisfies rubyzip and `unzip`, but Excel reports
|
|
11
|
+
a corrupt file and LibreOffice 7.4 fails with "source file could not be
|
|
12
|
+
loaded", which also broke conversion to PDF. Newer LibreOffice reads ZIP64,
|
|
13
|
+
so the breakage only surfaced on hosts with an older build. The member size
|
|
14
|
+
is known at write time and is now declared up front, which keeps ordinary
|
|
15
|
+
local headers. rubyzip 2.x was never affected: ZIP64 is off by default there.
|
|
16
|
+
|
|
17
|
+
## 0.2.0 (2026-08-04)
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Empty rows omitted from `sheetData` no longer collapse. Spreadsheet
|
|
22
|
+
applications skip blank rows when writing a sheet, so row numbers arrive with
|
|
23
|
+
gaps; the renderer used to renumber output rows contiguously and silently
|
|
24
|
+
dropped that vertical space. Templates that use blank rows as spacers — print
|
|
25
|
+
forms in particular — drifted upwards, and multi-page documents repaginated.
|
|
26
|
+
The gaps are now materialized as empty placeholder rows.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- `filters:` render option — pass a Liquid filter module (or an array of them)
|
|
31
|
+
to make host-application filters available in every cell. The modules are
|
|
32
|
+
added to the render context only, so nothing is registered on
|
|
33
|
+
`Liquid::Environment` and the host's global Liquid config stays untouched.
|
|
34
|
+
|
|
3
35
|
## 0.1.0 (2026-08-04)
|
|
4
36
|
|
|
5
37
|
First public release.
|
data/README.md
CHANGED
|
@@ -689,7 +689,8 @@ LiquidXlsx.render(
|
|
|
689
689
|
hide_control_sheets: true, # скрывать контрольные листы
|
|
690
690
|
hide_template_sheets: false, # скрывать листы-шаблоны после клонирования
|
|
691
691
|
images: nil, # { loader:, default_dpi: } для {% image_tag %}
|
|
692
|
-
liquid_resource_limits: nil
|
|
692
|
+
liquid_resource_limits: nil, # { render_length_limit:, render_score_limit:, ... }
|
|
693
|
+
filters: nil # модуль (или массив модулей) со своими Liquid-фильтрами
|
|
693
694
|
)
|
|
694
695
|
```
|
|
695
696
|
|
|
@@ -703,6 +704,28 @@ LiquidXlsx.render(
|
|
|
703
704
|
| `hide_template_sheets` | `false` | Скрывать листы-шаблоны |
|
|
704
705
|
| `images` | `nil` | `{ loader:, default_dpi: }` — загрузчик путей и DPI для `width/height` |
|
|
705
706
|
| `liquid_resource_limits` | `nil` | Лимиты Liquid (`render_length_limit`, `render_score_limit`, `assign_score_limit`) |
|
|
707
|
+
| `filters` | `nil` | Модуль или массив модулей со своими фильтрами — см. [Свои фильтры](#свои-фильтры) |
|
|
708
|
+
|
|
709
|
+
### Свои фильтры
|
|
710
|
+
|
|
711
|
+
Фильтры приложения подключаются опцией `filters:` и доступны в любой ячейке книги:
|
|
712
|
+
|
|
713
|
+
```ruby
|
|
714
|
+
module MyFilters
|
|
715
|
+
def money2(value) = format("%.2f", value.to_f)
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
LiquidXlsx.render(template: "invoice.xlsx", output: "out.xlsx",
|
|
719
|
+
data: data, filters: MyFilters) # или [MyFilters, DateFilters]
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
```
|
|
723
|
+
{{ invoice.sum | money2 }}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
Модули добавляются только в контекст рендера (`Liquid::Context#add_filters`),
|
|
727
|
+
на `Liquid::Environment` ничего не регистрируется — глобальная конфигурация
|
|
728
|
+
Liquid в приложении остаётся нетронутой.
|
|
706
729
|
|
|
707
730
|
## Обработка ошибок
|
|
708
731
|
|
data/lib/liquid_xlsx/package.rb
CHANGED
|
@@ -23,6 +23,17 @@ module LiquidXlsx
|
|
|
23
23
|
name == :create && %i[key keyreq].include?(type)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# rubyzip 3 turned ZIP64 on by default. Streaming a member gives it no size
|
|
27
|
+
# up front, so it marks EVERY member as ZIP64 — version 4.5 in the local
|
|
28
|
+
# header and 0xFFFFFFFF sizes. Such an .xlsx is rejected by Excel and by
|
|
29
|
+
# LibreOffice builds without ZIP64 support (7.4 fails, 26.2 reads it).
|
|
30
|
+
# Here the member content is fully known, so the size can be declared up
|
|
31
|
+
# front and Entry#prep_local_zip64_extra leaves an ordinary header alone.
|
|
32
|
+
# rubyzip 2.x has no `size:` keyword, but there ZIP64 is off by default.
|
|
33
|
+
ZIP_SIZE_KEYWORD = Zip::File.instance_method(:get_output_stream).parameters.any? do |type, name|
|
|
34
|
+
name == :size && %i[key keyreq].include?(type)
|
|
35
|
+
end
|
|
36
|
+
|
|
26
37
|
# Guard rails against zip bombs / resource exhaustion when unpacking.
|
|
27
38
|
MAX_ENTRIES = 10_000
|
|
28
39
|
MAX_ENTRY_UNCOMPRESSED = 512 * 1024 * 1024 # 512 MB per entry
|
|
@@ -107,7 +118,7 @@ module LiquidXlsx
|
|
|
107
118
|
@files.each do |name, content|
|
|
108
119
|
next if content.nil? # directories
|
|
109
120
|
|
|
110
|
-
zip
|
|
121
|
+
write_entry(zip, name, content)
|
|
111
122
|
end
|
|
112
123
|
end
|
|
113
124
|
end
|
|
@@ -443,6 +454,17 @@ module LiquidXlsx
|
|
|
443
454
|
|
|
444
455
|
private
|
|
445
456
|
|
|
457
|
+
# The size is declared in bytes: String#size counts characters, so on UTF-8
|
|
458
|
+
# content it would disagree with what actually gets written.
|
|
459
|
+
# See ZIP_SIZE_KEYWORD — rubyzip 2.x has no such keyword and needs none.
|
|
460
|
+
def write_entry(zip, name, content)
|
|
461
|
+
if ZIP_SIZE_KEYWORD
|
|
462
|
+
zip.get_output_stream(name, size: content.bytesize) { |f| f.write(content) }
|
|
463
|
+
else
|
|
464
|
+
zip.get_output_stream(name) { |f| f.write(content) }
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
446
468
|
# Open a brand-new archive for writing across rubyzip 2.x and 3.x.
|
|
447
469
|
# See ZIP_CREATE_KEYWORD for why the call has to be spelled two ways.
|
|
448
470
|
def open_new_zip(output_path, &)
|
data/lib/liquid_xlsx/renderer.rb
CHANGED
|
@@ -97,6 +97,11 @@ module LiquidXlsx
|
|
|
97
97
|
end
|
|
98
98
|
liquid_context.strict_variables = strict_vars
|
|
99
99
|
liquid_context.strict_filters = strict_filts
|
|
100
|
+
# Host-application filters, if any. Scoped to this context — nothing is
|
|
101
|
+
# registered on Liquid::Environment, so the host's global config is
|
|
102
|
+
# neither required nor mutated.
|
|
103
|
+
extra_filters = @options[:filters]
|
|
104
|
+
liquid_context.add_filters(extra_filters) if extra_filters
|
|
100
105
|
apply_resource_limits(liquid_context)
|
|
101
106
|
|
|
102
107
|
# Get original merge cells
|
data/lib/liquid_xlsx/version.rb
CHANGED
|
@@ -56,6 +56,14 @@ module LiquidXlsx
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# Parse rows from the worksheet.
|
|
59
|
+
#
|
|
60
|
+
# Spreadsheet applications omit empty rows from sheetData, so row numbers
|
|
61
|
+
# come with gaps. Those gaps are not nothing: an empty row still takes up
|
|
62
|
+
# vertical space on the sheet, and templates use them as spacers. Since the
|
|
63
|
+
# renderer renumbers output rows contiguously, the gaps are materialized
|
|
64
|
+
# here as empty placeholder rows — otherwise every spacer collapses and the
|
|
65
|
+
# whole document creeps upwards.
|
|
66
|
+
#
|
|
59
67
|
# @return [Array<Hash>] rows with :row_number, :row_xml, :cells, :row_attrs
|
|
60
68
|
def parse_rows
|
|
61
69
|
rows = []
|
|
@@ -68,6 +76,9 @@ module LiquidXlsx
|
|
|
68
76
|
@doc.xpath("//xmlns:sheetData/xmlns:row", "xmlns" => ns.href).each do |row_elem|
|
|
69
77
|
row_num = row_elem["r"]&.to_i
|
|
70
78
|
row_num = last_row_num + 1 if row_num.nil? || row_num < 1
|
|
79
|
+
|
|
80
|
+
((last_row_num + 1)...row_num).each { |missing| rows << placeholder_row(missing) }
|
|
81
|
+
|
|
71
82
|
last_row_num = row_num
|
|
72
83
|
cells = parse_cells(row_elem, row_num)
|
|
73
84
|
rows << {
|
|
@@ -80,6 +91,17 @@ module LiquidXlsx
|
|
|
80
91
|
rows
|
|
81
92
|
end
|
|
82
93
|
|
|
94
|
+
# An empty row omitted from sheetData. No attributes — the sheet's
|
|
95
|
+
# defaultRowHeight applies, exactly as the spreadsheet application shows it.
|
|
96
|
+
def placeholder_row(row_num)
|
|
97
|
+
{
|
|
98
|
+
row_number: row_num,
|
|
99
|
+
row_xml: nil,
|
|
100
|
+
cells: [],
|
|
101
|
+
row_attrs: {}
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
83
105
|
# Get merge cells from the worksheet.
|
|
84
106
|
# @return [Array<Hash>] each: {ref: "A3:B3", start_row:, end_row:}
|
|
85
107
|
def merge_cells
|
data/lib/liquid_xlsx.rb
CHANGED
|
@@ -64,13 +64,16 @@ module LiquidXlsx
|
|
|
64
64
|
# @param images [Hash] image rendering options: { loader:, default_dpi: }
|
|
65
65
|
# @param liquid_resource_limits [Hash, nil] optional Liquid resource limits:
|
|
66
66
|
# { render_length_limit:, render_score_limit:, assign_score_limit: }
|
|
67
|
+
# @param filters [Module, Array<Module>, nil] extra Liquid filter modules made
|
|
68
|
+
# available to every cell of the workbook. Added to the render context only,
|
|
69
|
+
# so nothing is registered globally on Liquid::Environment.
|
|
67
70
|
# rubocop:disable Metrics/ParameterLists
|
|
68
71
|
def render(template:, output:, data:,
|
|
69
72
|
strict_variables: false, strict_filters: false,
|
|
70
73
|
recalculate_formulas: false, remove_template_comments: false,
|
|
71
74
|
dynamic_sheets: false, hide_control_sheets: true,
|
|
72
75
|
hide_template_sheets: false, images: nil,
|
|
73
|
-
liquid_resource_limits: nil)
|
|
76
|
+
liquid_resource_limits: nil, filters: nil)
|
|
74
77
|
tpl = Template.new(template, {
|
|
75
78
|
strict_variables: strict_variables,
|
|
76
79
|
strict_filters: strict_filters,
|
|
@@ -80,7 +83,8 @@ module LiquidXlsx
|
|
|
80
83
|
hide_control_sheets: hide_control_sheets,
|
|
81
84
|
hide_template_sheets: hide_template_sheets,
|
|
82
85
|
images: images || {},
|
|
83
|
-
liquid_resource_limits: liquid_resource_limits
|
|
86
|
+
liquid_resource_limits: liquid_resource_limits,
|
|
87
|
+
filters: filters
|
|
84
88
|
})
|
|
85
89
|
# rubocop:enable Metrics/ParameterLists
|
|
86
90
|
tpl.render_to_file(data, output)
|