Class: LiquidXlsx::Renderer
- Inherits:
-
Object
- Object
- LiquidXlsx::Renderer
- Defined in:
- lib/liquid_xlsx/renderer.rb
Overview
Renders a single worksheet by parsing the template AST and rebuilding the sheet data with Liquid-rendered values.
Constant Summary collapse
- SINGLE_VAR =
Pattern for a simple variable-only template (no filters, no mixed text).
/\A\{\{\s*([\w.]+)\s*\}\}\z/- TEMPLATE_CACHE_MAX_SIZE =
Maximum size of the per-renderer Liquid::Template parse cache. When exceeded the entire cache is cleared (simple FIFO-like eviction).
512- DATE_1900_EPOCH =
Epoch for Excel 1900 date system (accounts for the leap-year bug). Serial = days since this date. For dates after 1900-02-28 the spurious 1900-02-29 absorbs the one-day offset, producing correct serials for all modern dates. TODO: support 1904 date system if workbookPr/@date1904 is accessible. Currently the Renderer has no access to package/workbook.xml; when date1904 detection is added, switch epoch to Date.new(1904,1,1).
Date.new(1899, 12, 30)
- SECONDS_PER_DAY =
86_400.0- DATE_TIME_CLASSES =
[Date, Time].freeze
Instance Attribute Summary collapse
-
#options ⇒ Object
readonly
rubocop:disable Metrics/ClassLength.
-
#sheet_r_id ⇒ Object
Returns the value of attribute sheet_r_id.
-
#worksheet ⇒ Object
readonly
rubocop:disable Metrics/ClassLength.
Instance Method Summary collapse
-
#initialize(worksheet, options = {}) ⇒ Renderer
constructor
A new instance of Renderer.
-
#render(data, workbook_ops: nil, image_ops: nil, extra_scope: nil) ⇒ String
Render the worksheet with data.
Constructor Details
#initialize(worksheet, options = {}) ⇒ Renderer
Returns a new instance of Renderer.
30 31 32 33 34 |
# File 'lib/liquid_xlsx/renderer.rb', line 30 def initialize(worksheet, = {}) @worksheet = worksheet @options = @template_cache = {} end |
Instance Attribute Details
#options ⇒ Object (readonly)
rubocop:disable Metrics/ClassLength
9 10 11 |
# File 'lib/liquid_xlsx/renderer.rb', line 9 def @options end |
#sheet_r_id ⇒ Object
Returns the value of attribute sheet_r_id.
10 11 12 |
# File 'lib/liquid_xlsx/renderer.rb', line 10 def sheet_r_id @sheet_r_id end |
#worksheet ⇒ Object (readonly)
rubocop:disable Metrics/ClassLength
9 10 11 |
# File 'lib/liquid_xlsx/renderer.rb', line 9 def worksheet @worksheet end |
Instance Method Details
#render(data, workbook_ops: nil, image_ops: nil, extra_scope: nil) ⇒ String
Render the worksheet with data.
data may be a Hash, a Liquid::Drop, or any object responding to
#to_liquid that returns a Hash or a Drop. Plain objects without [] /
#key? are not supported because Liquid::Context looks variables up via
these methods on the root environment.
extra_scope adds variables that take priority over data (used by
dynamic sheets to inject the per-op local variable without merging —
which would otherwise require data to be a Hash). It is passed as the
second argument of Liquid::Context, the outer scope, which has higher
precedence than environments.
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/liquid_xlsx/renderer.rb', line 55 def render(data, workbook_ops: nil, image_ops: nil, extra_scope: nil) rows = worksheet.parse_rows return worksheet.to_xml if rows.empty? # Quick scan: skip sheet if no Liquid tags at all = rows.any? do |r| r[:cells].any? { |c| c[:template] } end return worksheet.to_xml unless # Build AST parser = TemplateParser.new(rows, worksheet.name) ast = parser.parse strict_vars = @options.fetch(:strict_variables, false) strict_filts = @options.fetch(:strict_filters, false) # One Liquid::Context per worksheet — shared across all cells. # Normalize arbitrary #to_liquid objects (AR models, Struct, …) into a # Hash or Drop the context can look variables up in. Hash and Drop pass # through unchanged (Drop#to_liquid returns self). normalized = normalize_data(data) data_hash = stringify_keys(normalized) registers = { liquid_xlsx_workbook_ops: workbook_ops, liquid_xlsx_image_ops: image_ops || [], liquid_xlsx_dynamic_sheets: @options.fetch(:dynamic_sheets, false), liquid_xlsx_sheet_name: @worksheet.name, liquid_xlsx_sheet_r_id: @sheet_r_id } liquid_context = ::Liquid::Context.new(data_hash, {}, registers, true) # `extra_scope` is applied AFTER context construction on purpose: passing # it as the second (outer_scope) argument would let Liquid's # `squash_instance_assigns_with_environments` (invoked at the end of # `Context#initialize`) shadow each key with the corresponding value # from the environment. For a Drop environment that is fatal, because # `Drop#key?` always returns `true`, so unknown methods yield `nil` and # overwrite the explicit extra variables. Assigning directly onto the # context bypasses squash entirely and still wins lookup priority # because scopes are searched before environments. if extra_scope stringify_keys(extra_scope).each do |k, v| liquid_context[k] = v end end liquid_context.strict_variables = strict_vars liquid_context.strict_filters = strict_filts # Host-application filters, if any. Scoped to this context — nothing is # registered on Liquid::Environment, so the host's global config is # neither required nor mutated. extra_filters = @options[:filters] liquid_context.add_filters(extra_filters) if extra_filters apply_resource_limits(liquid_context) # Get original merge cells original_merges = worksheet.merge_cells merge_transformer = MergeCellsTransformer.new # Render AST to produce new rows + merge info formula_translator = FormulaTranslator.new state = RenderState.new(merge_transformer, original_merges, formula_translator) rendered = render_ast(ast, liquid_context, data_hash, rows, strict_filts, state) # Check for merge cells crossing structural block boundaries state.check_block_boundaries(worksheet.name) # Build merge ranges list merge_ranges = state.build_merge_ranges # Translate formulas now that all loop expansions are known finalize_formulas(rendered, state.formula_translator) # Rebuild sheet data (always: an empty result must clear template rows too) worksheet.rebuild_sheet_data(rendered, 1) # Update dimension worksheet.update_dimension(rendered.length) # Always update merge cells (clears old ones even if result is empty) worksheet.update_merge_cells(merge_ranges) # Remove cached formula values if recalculating if @options[:recalculate_formulas] worksheet.remove_cached_formula_values end worksheet.to_xml end |