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)


805
806
807
808
809
810
811
812
813
814
815
# File 'lib/xlsxrb.rb', line 805

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)


990
991
992
# File 'lib/xlsxrb.rb', line 990

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.



932
933
934
# File 'lib/xlsxrb.rb', line 932

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

#buildElements::Workbook

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

: () -> Elements::Workbook

Returns:



965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
# File 'lib/xlsxrb.rb', line 965

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.



921
922
923
# File 'lib/xlsxrb.rb', line 921

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)


956
957
958
# File 'lib/xlsxrb.rb', line 956

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)


866
867
868
869
870
# File 'lib/xlsxrb.rb', line 866

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 })


1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
# File 'lib/xlsxrb.rb', line 1075

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)


879
880
881
882
883
884
# File 'lib/xlsxrb.rb', line 879

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)


894
895
896
897
898
899
900
901
902
# File 'lib/xlsxrb.rb', line 894

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])


1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
# File 'lib/xlsxrb.rb', line 1009

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)


943
944
945
946
# File 'lib/xlsxrb.rb', line 943

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.



910
911
912
# File 'lib/xlsxrb.rb', line 910

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)


995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
# File 'lib/xlsxrb.rb', line 995

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:



841
842
843
844
845
846
847
848
849
850
851
852
# File 'lib/xlsxrb.rb', line 841

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.



828
829
830
# File 'lib/xlsxrb.rb', line 828

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