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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cdc976c8086895374758eae13fd1006379ffd15f37e3cad03cff012be6986065
4
- data.tar.gz: de8e167af7057eb7dfad00991610a3171a30a13b67ed2eda1fa43b1f00bd9358
3
+ metadata.gz: 2962349e5fe1197bffc9c0d28d21be73e83b97f71cf3e8ce86579c07246e5072
4
+ data.tar.gz: 67d36e10dfaf54790ffd949e175ba80b24940d331eb9e9b826661158b32c08ee
5
5
  SHA512:
6
- metadata.gz: c36433b43abf4b17f5a0fdbdcab009f6fb2a2d6d18c5c1c517c99a02f8a0207be1b3d3c2ba37187f57d96e1279083450666a3036d9281e5e7f9c3fa0f734ab34
7
- data.tar.gz: 7c8f204d3284aba334ed3f942b0ee34ac3dde5953ad413d8a9847c5d6b588f1aaf754a82f0ada0cf15af0958b15bb3330f4ad49c917cb20644da4cbc3ff75b1b
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 # { render_length_limit:, render_score_limit:, ... }
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
 
@@ -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.get_output_stream(name) { |f| f.write(content) }
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, &)
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LiquidXlsx
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.1"
5
5
  end
@@ -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)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: liquid_xlsx
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Khlipitkin