Class: Xlsxrb::WorkbookBuilder

Inherits:
Object
  • Object
show all
Defined in:
lib/xlsxrb.rb,
sig/generated/xlsxrb.rbs

Overview

DSL context for Xlsxrb.build.

Instance Method Summary collapse

Constructor Details

#initialize(strict_excel_mode: true) ⇒ WorkbookBuilder

: (?strict_excel_mode: bool) -> void

Parameters:

  • strict_excel_mode: (Boolean) (defaults to: true)


656
657
658
659
660
661
662
663
664
665
666
# File 'lib/xlsxrb.rb', line 656

def initialize(strict_excel_mode: true)
  @strict_excel_mode = strict_excel_mode
  @sheets = []
  @sheet_builders = [] # Keep track of sheet builders for style processing
  @defined_names = []
  @core_properties = {}
  @app_properties = {}
  @custom_properties = []
  @workbook_protection = nil
  @workbook_properties = { update_links: "never" }
end

Instance Method Details

#absolute_range(range) ⇒ Object

: (untyped range) -> untyped

Parameters:

  • range (Object)

Returns:

  • (Object)


841
842
843
# File 'lib/xlsxrb.rb', line 841

def absolute_range(range)
  range.gsub(/([A-Z]+)(\d+)/, '$\1$\2')
end

#app_property(name, value) ⇒ void

This method returns an undefined value.

Set an app document property.

: (Symbol name, String | Integer | Time value) -> void

Parameters:

  • name (Symbol)

    The property name.

  • value (String, Integer, Time)

    The property value.



783
784
785
# File 'lib/xlsxrb.rb', line 783

def app_property(name, value)
  @app_properties[name] = value
end

#buildElements::Workbook

Builds and returns the in-memory Elements::Workbook.

: () -> Elements::Workbook

Returns:



816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
# File 'lib/xlsxrb.rb', line 816

def build
  raise ArgumentError, "Workbook must contain at least one sheet (Excel limitation)" if @strict_excel_mode && @sheets.empty?

  # Process styles from all sheets and collect style definitions
  processed_sheets, styles_definition = process_styles(@sheets)

  # Store workbook-level metadata in unmapped_data
  wb_meta = {}
  wb_meta[:defined_names] = resolve_defined_names(@defined_names, processed_sheets) unless @defined_names.empty?
  wb_meta[:core_properties] = @core_properties unless @core_properties.empty?
  wb_meta[:app_properties] = @app_properties unless @app_properties.empty?
  wb_meta[:custom_properties] = @custom_properties unless @custom_properties.empty?
  wb_meta[:workbook_protection] = @workbook_protection if @workbook_protection
  wb_meta[:workbook_properties] = @workbook_properties unless @workbook_properties.empty?

  Elements::Workbook.new(
    sheets: processed_sheets,
    styles: styles_definition,
    unmapped_data: wb_meta.empty? ? {} : { facade: wb_meta }
  )
end

#core_property(name, value) ⇒ void

This method returns an undefined value.

Set a core document property.

: (Symbol name, String | Integer | Time value) -> void

Parameters:

  • name (Symbol)

    The property name.

  • value (String, Integer, Time)

    The property value.



772
773
774
# File 'lib/xlsxrb.rb', line 772

def core_property(name, value)
  @core_properties[name] = value
end

#custom_property(name, value, type: :string) ⇒ void

This method returns an undefined value.

Add a custom document property.

: (String name, String | Integer | Float | bool | Time value, ?type: ::Symbol) -> void

Parameters:

  • name (String)

    The property name.

  • value (String, Integer, Float, Boolean, Time)

    The property value.

  • type (Symbol) (defaults to: :string)

    The type of property (:string, :number, :bool, :date).

  • type: (::Symbol) (defaults to: :string)


807
808
809
# File 'lib/xlsxrb.rb', line 807

def custom_property(name, value, type: :string)
  @custom_properties << { name: name, value: value, type: type }
end

#defined_name(name, value, sheet: nil, hidden: false) ⇒ void

This method returns an undefined value.

Add a defined name.

: (String name, String value, ?sheet: String?, ?hidden: bool) -> void

Parameters:

  • name (String)

    The defined name.

  • value (String)

    The formula or value.

  • sheet (String, nil) (defaults to: nil)

    Local sheet name.

  • hidden (Boolean) (defaults to: false)

    Whether the defined name is hidden.

  • sheet: (String, nil) (defaults to: nil)
  • hidden: (Boolean) (defaults to: false)


717
718
719
720
721
# File 'lib/xlsxrb.rb', line 717

def defined_name(name, value, sheet: nil, hidden: false)
  entry = { name: name, value: value, hidden: hidden }
  entry[:local_sheet_name] = sheet if sheet
  @defined_names << entry
end

#extract_styles_from_writer(writer) ⇒ { fonts: untyped, fills: untyped, borders: untyped, xf_entries: untyped, num_fmts: untyped }

: (untyped writer) -> { fonts: untyped, fills: untyped, borders: untyped, xf_entries: untyped, num_fmts: untyped }

Parameters:

  • writer (Object)

Returns:

  • ({ fonts: untyped, fills: untyped, borders: untyped, xf_entries: untyped, num_fmts: untyped })


926
927
928
929
930
931
932
933
934
935
936
# File 'lib/xlsxrb.rb', line 926

def extract_styles_from_writer(writer)
  # Extract style definitions from the writer that can be reused
  # This captures the fonts, fills, borders, and xf entries that were created
  {
    fonts: writer.fonts.dup,
    fills: writer.fills.dup,
    borders: writer.borders.dup,
    xf_entries: writer.xf_entries.dup,
    num_fmts: writer.num_fmts.dup
  }
end

This method returns an undefined value.

Set the print area for a sheet.

: (String range, ?sheet: String?) -> void

Parameters:

  • range (String)

    The range string (e.g. "A1:B10").

  • sheet (String, nil) (defaults to: nil)

    The sheet name.

  • sheet: (String, nil) (defaults to: nil)


730
731
732
733
734
735
# File 'lib/xlsxrb.rb', line 730

def print_area(range, sheet: nil)
  sheet_name = sheet || @sheets.last&.name || "Sheet1"
  value = "'#{sheet_name}'!#{absolute_range(range)}"
  @defined_names.reject! { |dn| dn[:name] == "_xlnm.Print_Area" && dn[:local_sheet_name] == sheet_name }
  defined_name("_xlnm.Print_Area", value, sheet: sheet_name)
end

This method returns an undefined value.

Set print titles for a sheet.

: (?rows: String?, ?cols: String?, ?sheet: String?) -> void

Parameters:

  • rows (String, nil) (defaults to: nil)

    Rows to repeat (e.g. "1:2").

  • cols (String, nil) (defaults to: nil)

    Columns to repeat (e.g. "A:B").

  • sheet (String, nil) (defaults to: nil)

    The sheet name.

  • rows: (String, nil) (defaults to: nil)
  • cols: (String, nil) (defaults to: nil)
  • sheet: (String, nil) (defaults to: nil)


745
746
747
748
749
750
751
752
753
# File 'lib/xlsxrb.rb', line 745

def print_titles(rows: nil, cols: nil, sheet: nil)
  sheet_name = sheet || @sheets.last&.name || "Sheet1"
  parts = []
  parts << "'#{sheet_name}'!$#{cols.sub(":", ":$")}" if cols
  parts << "'#{sheet_name}'!$#{rows.sub(":", ":$")}" if rows
  value = parts.join(",")
  @defined_names.reject! { |dn| dn[:name] == "_xlnm.Print_Titles" && dn[:local_sheet_name] == sheet_name }
  defined_name("_xlnm.Print_Titles", value, sheet: sheet_name)
end

#process_styles(sheets) ⇒ ::Array[untyped | ::Hash[untyped, untyped]], ::Array[untyped]

: (untyped sheets) -> (::Array[untyped | ::Hash[untyped, untyped]] | ::Array)

Parameters:

  • sheets (Object)

Returns:

  • (::Array[untyped | ::Hash[untyped, untyped]], ::Array[untyped])


860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
# File 'lib/xlsxrb.rb', line 860

def process_styles(sheets)
  # Collect all unique StyleBuilders from all sheets
  all_style_builders = {}
  @sheet_builders.each do |sheet_builder|
    sheet_builder.styles.each do |style_name, style_builder|
      all_style_builders[style_name] = style_builder
    end
  end

  return [sheets, {}] if all_style_builders.empty?

  # Create a temporary writer to register styles and get numeric IDs
  temp_writer = Ooxml::Writer.new
  style_name_to_id = {}
  all_style_builders.each do |name, builder|
    style_id = builder.register_with(temp_writer)
    style_name_to_id[name] = style_id
  end

  # Capture the style definitions from the temporary writer
  styles_definition = extract_styles_from_writer(temp_writer)

  # Update all cells with their resolved style IDs
  updated_sheets = sheets.map do |sheet|
    new_rows = sheet.rows.map do |row|
      new_cells = row.cells.map do |cell|
        # If style_index is a string (style name), resolve it to a numeric ID
        if cell.style_index.is_a?(String) && style_name_to_id.key?(cell.style_index)
          Elements::Cell.new(
            row_index: cell.row_index,
            column_index: cell.column_index,
            value: cell.value,
            formula: cell.formula,
            style_index: style_name_to_id[cell.style_index],
            unmapped_data: cell.unmapped_data,
            errors: cell.errors
          )
        else
          cell
        end
      end
      Elements::Row.new(
        index: row.index,
        cells: new_cells,
        height: row.height,
        hidden: row.hidden,
        custom_height: row.custom_height,
        outline_level: row.outline_level,
        unmapped_data: row.unmapped_data,
        errors: row.errors
      )
    end
    Elements::Worksheet.new(
      name: sheet.name,
      rows: new_rows,
      columns: sheet.columns,
      charts: sheet.charts,
      unmapped_data: sheet.unmapped_data,
      errors: sheet.errors
    )
  end

  [updated_sheets, styles_definition]
end

#properties(core: nil, app: nil) ⇒ void

This method returns an undefined value.

Set multiple core and/or app properties.

: (?core: Hash[Symbol, String | Integer | Time]?, ?app: Hash[Symbol, String | Integer | Time]?) -> void

Parameters:

  • core (Hash, nil) (defaults to: nil)

    Core properties.

  • app (Hash, nil) (defaults to: nil)

    App properties.

  • core: (Hash[Symbol, String | Integer | Time], nil) (defaults to: nil)
  • app: (Hash[Symbol, String | Integer | Time], nil) (defaults to: nil)


794
795
796
797
# File 'lib/xlsxrb.rb', line 794

def properties(core: nil, app: nil)
  core&.each { |k, v| core_property(k, v) }
  app&.each { |k, v| app_property(k, v) }
end

#protect_workbook(**opts) ⇒ void

This method returns an undefined value.

Set workbook protection.

: (**String | Integer | bool | nil opts) -> void

Parameters:

  • opts (Hash)

    Protection options.



761
762
763
# File 'lib/xlsxrb.rb', line 761

def protect_workbook(**opts)
  @workbook_protection = opts
end

#resolve_defined_names(names, sheets) ⇒ Object

: (untyped names, untyped sheets) -> untyped

Parameters:

  • names (Object)
  • sheets (Object)

Returns:

  • (Object)


846
847
848
849
850
851
852
853
854
855
856
857
# File 'lib/xlsxrb.rb', line 846

def resolve_defined_names(names, sheets)
  sheet_names = sheets.map(&:name)
  names.map do |dn|
    resolved = dn.dup
    if dn[:local_sheet_name]
      idx = sheet_names.index(dn[:local_sheet_name])
      resolved[:local_sheet_id] = idx if idx
      resolved.delete(:local_sheet_name)
    end
    resolved
  end
end

#sheet(name = nil, **opts) {|sheet_builder| ... } ⇒ void Also known as: []

This method returns an undefined value.

Add a new sheet.

: (?String? name, **untyped opts) ?{ (WorksheetBuilder) -> void } -> untyped

Parameters:

  • name (String, nil) (defaults to: nil)

    The name of the sheet.

  • opts (Hash)

    Sheet properties.

Yields:

  • (sheet_builder)

Yield Parameters:



692
693
694
695
696
697
698
699
700
701
702
703
# File 'lib/xlsxrb.rb', line 692

def sheet(name = nil, **opts)
  name ||= "Sheet#{@sheets.size + 1}"
  raise ArgumentError, "Sheet name '#{name}' must be <= 31 characters (Excel limitation)" if @strict_excel_mode && name.length > 31
  raise ArgumentError, "Sheet name '#{name}' contains invalid characters (ECMA-376 OOXML specification)" if name.match?(%r{[\[\]*?/\\]})
  raise ArgumentError, "Sheet name '#{name}' is already used. Excel requires unique sheet names." if @strict_excel_mode && @sheets.map { |s| s.respond_to?(:name) ? s.name.downcase : s.to_s.downcase }.include?(name.downcase)

  sheet_builder = WorksheetBuilder.new(name, strict_excel_mode: @strict_excel_mode)
  opts.each { |k, v| sheet_builder.sheet_properties(k, v) }
  yield sheet_builder if block_given?
  @sheet_builders << sheet_builder
  @sheets << sheet_builder.build
end

#workbook_property(name, value) ⇒ void

Note:

SECURITY WARNING: If you set :update_links to anything other than "never", you may expose end-users to malicious external reference vulnerabilities (e.g., CSV/DDE Injection) when they open the generated Excel file. Ensure you fully trust the exported data.

This method returns an undefined value.

Set a workbook property.

: (Symbol name, String | Integer | bool value) -> (String | Integer | bool)

Parameters:

  • name (Symbol)

    The property name (e.g. :update_links).

  • value (String, Integer, Boolean)

    The property value.



679
680
681
# File 'lib/xlsxrb.rb', line 679

def workbook_property(name, value)
  @workbook_properties[name] = value
end