Class: LiquidXlsx::Tags::SheetTag

Inherits:
Liquid::Tag
  • Object
show all
Defined in:
lib/liquid_xlsx/tags/sheet_tag.rb

Overview

Handles the sheet % Liquid tag for dynamic sheet creation.

Syntax:

{% sheet name: <expr> template: "SheetName" data: <expr> as: "var_name" %}
  • name: Liquid expression for the new sheet name (e.g. "invoice.number")
  • template: quoted string — name of existing template sheet to clone
  • data: Liquid expression — object to pass as local variable
  • as: (optional) quoted string — local variable name, defaults to "item"

Constant Summary collapse

SYNTAX =
/\A\s*(.+)\s*\z/
KNOWN_KEYS =

Known argument keys. Values are split on the positions of these keys (not on whitespace) so that a name/data value may itself contain spaces and Liquid filters, e.g.: name: inv.number | append: " - "

%w[name template data as].freeze
KEY_PATTERN =
/\b(?:name|template|data|as)\s*:/

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(tag_name, markup, options) ⇒ SheetTag

Returns a new instance of SheetTag.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/liquid_xlsx/tags/sheet_tag.rb', line 26

def initialize(tag_name, markup, options)
  super

  match = markup.match(SYNTAX)
  unless match
    raise_template_err(
      "Invalid sheet tag syntax. Expected: {% sheet name: ... template: ... data: ... %}"
    )
  end

  @args = parse_args(match[1])

  raise_template_err("Missing required argument: name") unless @args["name"]
  raise_template_err("Missing required argument: template") unless @args["template"]
  raise_template_err("Missing required argument: data") unless @args["data"]

  @name_arg = @args["name"]
  @template_name = @args["template"][:value]
  @data_arg = @args["data"]

  # Validate that template and as are literals
  raise_template_err("template must be a quoted string literal") unless @args["template"][:quoted]
  raise_template_err("as must be a valid identifier") \
    if @args["as"] && !@args["as"][:value].match?(/\A[A-Za-z_]\w*\z/)

  @as_name = @args["as"] ? @args["as"][:value] : "item"
end

Instance Attribute Details

#as_nameObject (readonly)

Exposed for Validator: it has to know which sheet the tag clones, to check that the sheet exists, without rendering the tag.



17
18
19
# File 'lib/liquid_xlsx/tags/sheet_tag.rb', line 17

def as_name
  @as_name
end

#template_nameObject (readonly)

Exposed for Validator: it has to know which sheet the tag clones, to check that the sheet exists, without rendering the tag.



17
18
19
# File 'lib/liquid_xlsx/tags/sheet_tag.rb', line 17

def template_name
  @template_name
end

Instance Method Details

#parse_args(markup) ⇒ Object

Split "name: EXPR template: "X" data: EXPR as: VAR" into a hash of { key => { value:, quoted: } }. Values may contain spaces and Liquid filters because we cut the markup at the positions of the known keys.

Keys are matched only OUTSIDE double-quoted strings, so a quoted value like name: "data: foo" is not mistaken for the data: key.



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
# File 'lib/liquid_xlsx/tags/sheet_tag.rb', line 60

def parse_args(markup)
  occurrences = []
  in_quote = false
  i = 0
  while i < markup.length
    ch = markup[i]
    if ch == '"'
      in_quote = !in_quote
      i += 1
      next
    end

    if !in_quote && (m = markup.match(KEY_PATTERN, i)) && m.begin(0) == i
      key = m[0][/\A\w+/]
      occurrences << { key: key, key_start: i, val_start: m.end(0) }
      i = m.end(0)
      next
    end

    i += 1
  end
  occurrences.sort_by! { |o| o[:key_start] }

  args = {}
  occurrences.each_with_index do |occ, i|
    next_start = i + 1 < occurrences.length ? occurrences[i + 1][:key_start] : markup.length
    raw = markup[occ[:val_start], next_start - occ[:val_start]]
    raw = raw.strip.sub(/,\s*\z/, "").strip
    raise_template_err("Empty value for argument: #{occ[:key].inspect}") if raw.empty?
    raise_template_err("Duplicate argument: #{occ[:key].inspect}") if args.key?(occ[:key])

    quoted = raw.start_with?('"') && raw.end_with?('"') && raw.length >= 2
    val = quoted ? raw[1..-2] : raw
    args[occ[:key]] = { value: val, quoted: quoted }
  end
  args
end

#render(context) ⇒ Object



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
# File 'lib/liquid_xlsx/tags/sheet_tag.rb', line 98

def render(context)
  # Raise error if dynamic_sheets is disabled
  unless context.registers[:liquid_xlsx_dynamic_sheets]
    raise RenderError.new(
      "Dynamic sheet creation is disabled. Pass dynamic_sheets: true.",
      sheet: context.registers[:liquid_xlsx_sheet_name],
      row: context.registers[:liquid_xlsx_source_row],
      cell: context.registers[:liquid_xlsx_source_cell],
      template: "{% sheet ... %}"
    )
  end

  # Resolve name expression through Liquid context (supports filters)
  name_val = evaluate_arg(@name_arg, context, expression: true)
  name_val = name_val.to_s.strip

  # Resolve data expression through Liquid context (preserves raw object)
  data_val = evaluate_arg(@data_arg, context)

  # Build operation with source context for error reporting
  op = {
    op: :create_sheet,
    template_sheet: @template_name,
    name: name_val,
    data: data_val,
    as: @as_name,
    source_sheet: context.registers[:liquid_xlsx_sheet_name],
    source_row: context.registers[:liquid_xlsx_source_row],
    source_cell: context.registers[:liquid_xlsx_source_cell]
  }

  # Store operation in registers
  ops = context.registers[:liquid_xlsx_workbook_ops] ||= []
  ops << op

  ""
end