class Xlsxrb::Ooxml::WorksheetWriter
Generates worksheet XML for a list of rows. Supports streaming: rows can be written one at a time.
Constants
- COLUMN_LETTERS
- DOC_REL_NS
- INTEGER_STRINGS
- SSML_NS
Public Class Methods
(untyped io) → untyped
Source
# File lib/xlsxrb/ooxml/worksheet_writer.rb, line 29 def initialize(io) @io = io @builder = XmlBuilder.new(@io) @row_buffer = String.new(capacity: 65_536) @started = false @finished = false end
Public Instance Methods
(?drawing_rid: untyped, ?sheet_protection: untyped, ?auto_filter: untyped, ?filter_columns: untyped, ?sort_state: untyped, ?merge_cells: untyped, ?conditional_formats: untyped, ?data_validations: untyped, ?hyperlinks: untyped, ?print_options: untyped, ?page_margins: untyped, ?page_setup: untyped, ?header_footer: untyped, ?row_breaks: untyped, ?col_breaks: untyped, ?tables: untyped, ?table_start_rid: untyped, ?legacy_drawing_rid: untyped, ?sparkline_groups: untyped) → untyped
Source
# File lib/xlsxrb/ooxml/worksheet_writer.rb, line 548 def finish(drawing_rid: nil, sheet_protection: nil, auto_filter: nil, filter_columns: nil, sort_state: nil, merge_cells: nil, conditional_formats: nil, data_validations: nil, hyperlinks: nil, print_options: nil, page_margins: nil, page_setup: nil, header_footer: nil, row_breaks: nil, col_breaks: nil, tables: nil, table_start_rid: nil, legacy_drawing_rid: nil, sparkline_groups: nil) return if @finished start unless @started @finished = true if @row_buffer && !@row_buffer.empty? @io.write(@row_buffer) @row_buffer.clear end @builder.close_tag("sheetData") # Elements must appear in OOXML specification order after sheetData write_sheet_protection(sheet_protection) if sheet_protection write_auto_filter(auto_filter, filter_columns, sort_state) if auto_filter write_merge_cells(merge_cells) if merge_cells && !merge_cells.empty? write_conditional_formatting(conditional_formats) if conditional_formats && !conditional_formats.empty? write_data_validations(data_validations) if data_validations && !data_validations.empty? write_hyperlinks(hyperlinks) if hyperlinks && !hyperlinks.empty? write_print_options(print_options) if print_options && !print_options.empty? write_page_margins(page_margins) if page_margins write_page_setup(page_setup) if page_setup && !page_setup.empty? write_header_footer(header_footer) if header_footer && !header_footer.empty? write_row_breaks(row_breaks) if row_breaks && !row_breaks.empty? write_col_breaks(col_breaks) if col_breaks && !col_breaks.empty? @builder.empty_tag("drawing", { "r:id": drawing_rid }) if drawing_rid @builder.empty_tag("legacyDrawing", { "r:id": legacy_drawing_rid }) if legacy_drawing_rid write_table_parts(tables, table_start_rid) if tables && !tables.empty? write_sparklines(sparkline_groups) if sparkline_groups && !sparkline_groups.empty? @builder.close_tag("worksheet") end
Write the worksheet footer. Call once after all rows. Options for post-sheetData elements (in OOXML order):
(?columns: untyped, ?sheet_properties: untyped, ?freeze_pane: untyped, ?split_pane: untyped, ?selection: untyped, ?sheet_view: untyped) → untyped
Source
# File lib/xlsxrb/ooxml/worksheet_writer.rb, line 44 def start(columns: [], sheet_properties: nil, freeze_pane: nil, split_pane: nil, selection: nil, sheet_view: nil) return if @started @started = true @builder.declaration @builder.open_tag("worksheet", { xmlns: SSML_NS, "xmlns:r": DOC_REL_NS }) write_sheet_properties(sheet_properties) if sheet_properties && !sheet_properties.empty? write_sheet_views(freeze_pane: freeze_pane, split_pane: split_pane, selection: selection, sheet_view: sheet_view) if freeze_pane || split_pane || selection || (sheet_view && !sheet_view.empty?) write_columns(columns) unless columns.empty? @builder.open_tag("sheetData") end
Write the worksheet header. Call once before writing rows. Options for pre-sheetData elements:
sheet_properties: Hash of sheet-level properties (:tab_color, etc.)
freeze_pane: { row:, col:, state: :frozen }
split_pane: { x_split:, y_split:, top_left_cell: }
selection: { active_cell:, sqref:, pane: }
sheet_view: Hash of sheet view properties
(untyped row_index, untyped cells, ?attrs: untyped, ?unmapped: untyped, ?sst_index: untyped) → untyped
Source
# File lib/xlsxrb/ooxml/worksheet_writer.rb, line 59 def write_row(row_index, cells, attrs: {}, unmapped: [], sst_index: nil) start unless @started row_num = row_index + 1 row_num_str = row_num.to_s buf = @row_buffer ||= String.new(capacity: 65_536) buf << '<row r="' << row_num_str << '"' if attrs[:height] buf << ' ht="' << attrs[:height].to_s << '" customHeight="1"' elsif attrs[:custom_height] buf << ' customHeight="1"' end buf << ' hidden="1"' if attrs[:hidden] buf << ' outlineLevel="' << attrs[:outline_level].to_s << '"' if attrs[:outline_level] buf << ">" cells.each do |cell| if cell.is_a?(Elements::Cell) value = cell.value style_id = cell.style_index col_ref = cell.ref || "#{column_letter(cell.column_index)}#{row_num_str}" formula = cell.formula formula_ca = false cell_type_val = nil elsif cell.is_a?(Hash) value = cell[:value] style_id = cell[:style_index] col_ref = cell[:ref] || "#{column_letter(cell[:column_index])}#{row_num_str}" formula = cell[:formula] formula_ca = cell[:formula_ca] cell_type_val = cell[:type] else value = cell style_id = nil col_ref = nil formula = nil formula_ca = false cell_type_val = nil end # Fast path for common unstyled cells if !formula && !style_id && !cell_type_val case value when Integer, Float buf << '<c r="' << col_ref << '"><v>' << value.to_s << "</v></c>" next when String if !value.start_with?("=") && sst_index && (idx = sst_index[value]) buf << '<c r="' << col_ref << '" t="s"><v>' << idx.to_s << "</v></c>" next end when true buf << '<c r="' << col_ref << '" t="b"><v>1</v></c>' next when false buf << '<c r="' << col_ref << '" t="b"><v>0</v></c>' next when nil buf << '<c r="' << col_ref << '"/>' next when Date serial = Xlsxrb::Ooxml::Utils.date_to_serial(value) buf << '<c r="' << col_ref << '"><v>' << serial.to_s << "</v></c>" next end end if value.nil? && formula.nil? buf << '<c r="' << col_ref << '"' buf << ' s="' << style_id.to_s << '"' if style_id buf << "/>" next end if value.is_a?(String) && value.start_with?("=") && !formula formula = value value = nil end xml_val = value type = cell_type_val formula_expr = nil if formula if formula.is_a?(Xlsxrb::Elements::Formula) formula_expr = formula.expression formula_ca = formula.calculate_always xml_val = formula.cached_value || value || nil else formula_expr = formula xml_val = value || nil end formula_expr = formula_expr[1..] if formula_expr.start_with?("=") end if !formula_expr && !type case value when String if sst_index && (idx = sst_index[value]) xml_val = idx type = "s" else type = "inlineStr" end when Xlsxrb::Elements::RichText if sst_index && (idx = sst_index[value]) xml_val = idx type = "s" else type = "inlineStr" xml_val = value end when true xml_val = "1" type = "b" when false xml_val = "0" type = "b" when Date xml_val = Xlsxrb::Ooxml::Utils.date_to_serial(value) when Time xml_val = Xlsxrb::Ooxml::Utils.datetime_to_serial(value) when BigDecimal xml_val = value.to_s("F") when Xlsxrb::Elements::CellError xml_val = value.code type = "e" end end buf << '<c r="' << col_ref << '"' buf << ' s="' << style_id.to_s << '"' if style_id buf << ' t="' << type << '"' if type if type == "inlineStr" if xml_val.is_a?(Xlsxrb::Elements::RichText) buf << "><is>" xml_val.runs.each do |run| font = run[:font] if font && !font.empty? buf << "<r><rPr>" buf << "<b/>" if font[:bold] buf << "<i/>" if font[:italic] buf << "<strike/>" if font[:strike] if font[:underline] if font[:underline] == true buf << "<u/>" else buf << '<u val="' << font[:underline].to_s << '"/>' end end buf << '<vertAlign val="' << font[:vert_align].to_s << '"/>' if font[:vert_align] buf << '<sz val="' << font[:sz].to_s << '"/>' if font[:sz] if font[:color] buf << '<color rgb="' << font[:color].to_s << '"/>' elsif font[:theme] tint_attr = font[:tint] ? " tint=\"#{font[:tint]}\"" : "" buf << '<color theme="' << font[:theme].to_s << '"' << tint_attr << "/>" end buf << '<rFont val="' << escape_xml(font[:name]) << '"/>' if font[:name] buf << '<family val="' << font[:family].to_s << '"/>' if font[:family] buf << '<scheme val="' << font[:scheme].to_s << '"/>' if font[:scheme] buf << "</rPr><t>" else buf << "<r><t>" end buf << escape_xml(run[:text]) << "</t></r>" end buf << "</is></c>" else buf << "><is><t>" << escape_xml(xml_val.to_s) << "</t></is></c>" end elsif formula_expr buf << if formula_ca '><f ca="1">' else "><f>" end buf << escape_xml(formula_expr) << "</f>" if xml_val buf << "<v>" << xml_val.to_s << "</v></c>" else buf << "</c>" end else buf << "><v>" << xml_val.to_s << "</v></c>" end end buf << "</row>" if buf.bytesize >= 32_768 @io.write(buf) buf.clear end unmapped.each { |node| @builder.write_unmapped(node) } end
Write a single row. Automatically calls start if needed.
(untyped row_index, untyped values, ?styles: untyped, ?style_map: untyped, ?sst: untyped, ?sst_index: untyped, ?attrs: untyped) → untyped
Source
# File lib/xlsxrb/ooxml/worksheet_writer.rb, line 258 def write_row_values(row_index, values, styles: nil, style_map: nil, sst: nil, sst_index: nil, attrs: nil) start unless @started row_num = row_index + 1 row_num_str = row_num < 65_536 ? INTEGER_STRINGS[row_num] : row_num.to_s buf = @row_buffer ||= String.new(capacity: 65_536) if styles.nil? && attrs.nil? buf << "<row r=\"#{row_num_str}\">" col_index = 0 max_len = values.length while col_index < max_len value = values[col_index] col_ref = COLUMN_LETTERS[col_index] || column_letter(col_index) col_index += 1 next if value.nil? case value when Integer val_str = value >= 0 && value < 65_536 ? INTEGER_STRINGS[value] : value.to_s buf << "<c r=\"#{col_ref}#{row_num_str}\"><v>#{val_str}</v></c>" when Float buf << "<c r=\"#{col_ref}#{row_num_str}\"><v>#{value}</v></c>" when String unless value.start_with?("=") raise ArgumentError, "Cell text length #{value.length} exceeds Excel limit of 32,767 characters" if @strict_excel_mode && value.length > 32_767 idx = (sst_index[value] ||= begin sst << value sst.size - 1 end) idx_str = idx < 65_536 ? INTEGER_STRINGS[idx] : idx.to_s buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"s\"><v>#{idx_str}</v></c>" next end formula_expr = value[1..] buf << "<c r=\"#{col_ref}#{row_num_str}\"><f>#{escape_xml(formula_expr)}</f></c>" when true buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"b\"><v>1</v></c>" when false buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"b\"><v>0</v></c>" when Date date_style_id = style_map ? style_map["__xlsxrb_date"] : nil buf << if date_style_id "<c r=\"#{col_ref}#{row_num_str}\" s=\"#{date_style_id}\"><v>#{Xlsxrb::Ooxml::Utils.date_to_serial(value)}</v></c>" else "<c r=\"#{col_ref}#{row_num_str}\"><v>#{Xlsxrb::Ooxml::Utils.date_to_serial(value)}</v></c>" end when Time time_style_id = style_map ? style_map["__xlsxrb_time"] : nil buf << if time_style_id "<c r=\"#{col_ref}#{row_num_str}\" s=\"#{time_style_id}\"><v>#{Xlsxrb::Ooxml::Utils.datetime_to_serial(value)}</v></c>" else "<c r=\"#{col_ref}#{row_num_str}\"><v>#{Xlsxrb::Ooxml::Utils.datetime_to_serial(value)}</v></c>" end when Xlsxrb::Elements::Formula formula_expr = value.expression formula_expr = formula_expr[1..] if formula_expr.start_with?("=") buf << if value.cached_value "<c r=\"#{col_ref}#{row_num_str}\"><f>#{escape_xml(formula_expr)}</f><v>#{value.cached_value}</v></c>" else "<c r=\"#{col_ref}#{row_num_str}\"><f>#{escape_xml(formula_expr)}</f></c>" end when Hash if value.key?(:formula) formula_expr = value[:formula] formula_expr = formula_expr[1..] if formula_expr.start_with?("=") xml_val = value[:value] buf << if xml_val "<c r=\"#{col_ref}#{row_num_str}\"><f>#{escape_xml(formula_expr)}</f><v>#{xml_val}</v></c>" else "<c r=\"#{col_ref}#{row_num_str}\"><f>#{escape_xml(formula_expr)}</f></c>" end end when Xlsxrb::Elements::CellError buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"e\"><v>#{value.code}</v></c>" when BigDecimal buf << "<c r=\"#{col_ref}#{row_num_str}\"><v>#{value.to_s("F")}</v></c>" else idx = (sst_index[value] ||= begin sst << value sst.size - 1 end) idx_str = idx < 65_536 ? INTEGER_STRINGS[idx] : idx.to_s buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"s\"><v>#{idx_str}</v></c>" end end buf << "</row>" return unless buf.bytesize >= 32_768 @io.write(buf) buf.clear return end is_styles_collection = styles && (styles.is_a?(Array) || styles.is_a?(Hash)) single_style_id = nil single_style_id = style_map[styles] if styles && style_map && !is_styles_collection buf << '<row r="' << row_num_str << '"' if attrs buf << ' ht="' << attrs[:height].to_s << '" customHeight="1"' if attrs[:height] buf << ' hidden="1"' if attrs[:hidden] buf << ' outlineLevel="' << attrs[:outline_level].to_s << '"' if attrs[:outline_level] end buf << ">" max_len = values.length if is_styles_collection styles_len = styles.is_a?(Array) ? styles.length : (styles.keys.max || -1) + 1 max_len = [max_len, styles_len].max end col_index = 0 while col_index < max_len value = col_index < values.length ? values[col_index] : nil style_id = single_style_id if is_styles_collection && style_map style_name = if styles.is_a?(Array) col_index < styles.length ? styles[col_index] : nil else styles[col_index] end style_id = style_map[style_name] if style_name end col_ref = COLUMN_LETTERS[col_index] || column_letter(col_index) if value.nil? buf << '<c r="' << col_ref << row_num_str << '" s="' << style_id.to_s << '"/>' if style_id col_index += 1 next end # Fast path: unstyled numbers, booleans, and simple strings (majority of cells) if style_id.nil? && !value.is_a?(Xlsxrb::Elements::Formula) && !value.is_a?(Hash) case value when Integer val_str = INTEGER_STRINGS[value] || value.to_s buf << '<c r="' << col_ref << row_num_str << '"><v>' << val_str << "</v></c>" col_index += 1 next when Float buf << '<c r="' << col_ref << row_num_str << '"><v>' << value.to_s << "</v></c>" col_index += 1 next when String unless value.start_with?("=") idx = sst_index[value] unless idx sst << value idx = sst.size - 1 sst_index[value] = idx end idx_str = INTEGER_STRINGS[idx] || idx.to_s buf << '<c r="' << col_ref << row_num_str << '" t="s"><v>' << idx_str << "</v></c>" col_index += 1 next end when true buf << '<c r="' << col_ref << row_num_str << '" t="b"><v>1</v></c>' col_index += 1 next when false buf << '<c r="' << col_ref << row_num_str << '" t="b"><v>0</v></c>' col_index += 1 next when Date buf << '<c r="' << col_ref << row_num_str << '"><v>' << Xlsxrb::Ooxml::Utils.date_to_serial(value).to_s << "</v></c>" col_index += 1 next when Time buf << '<c r="' << col_ref << row_num_str << '"><v>' << Xlsxrb::Ooxml::Utils.datetime_to_serial(value).to_s << "</v></c>" col_index += 1 next end end # General path: styled, formula, rich text, or complex cell formula_expr = nil formula_ca = false xml_val = value type = nil case value when Xlsxrb::Elements::Formula formula_expr = value.expression formula_ca = value.calculate_always xml_val = value.cached_value case value.cached_value when String type = "str" when true type = "b" xml_val = "1" when false type = "b" xml_val = "0" end when Hash if value.key?(:formula) formula_expr = value[:formula] formula_ca = value[:calculate_always] || false xml_val = value[:value] case xml_val when String type = "str" when true type = "b" xml_val = "1" when false type = "b" xml_val = "0" end end when String if value.start_with?("=") && value.length > 1 formula_expr = value xml_val = nil else idx = sst_index[value] unless idx sst << value idx = sst.size - 1 sst_index[value] = idx end xml_val = idx type = "s" end when Xlsxrb::Elements::RichText idx = sst_index[value] unless idx sst << value idx = sst.size - 1 sst_index[value] = idx end xml_val = idx type = "s" when true xml_val = "1" type = "b" when false xml_val = "0" type = "b" when Date xml_val = Xlsxrb::Ooxml::Utils.date_to_serial(value) when Time xml_val = Xlsxrb::Ooxml::Utils.datetime_to_serial(value) when BigDecimal xml_val = value.to_s("F") when Xlsxrb::Elements::CellError xml_val = value.code type = "e" end formula_expr = formula_expr[1..] if formula_expr&.start_with?("=") buf << '<c r="' << col_ref << row_num_str << '"' buf << ' s="' << style_id.to_s << '"' if style_id buf << ' t="' << type << '"' if type if formula_expr buf << if formula_ca '><f ca="1">' else "><f>" end buf << escape_xml(formula_expr) << "</f>" if xml_val buf << "<v>" << xml_val.to_s << "</v></c>" else buf << "</c>" end else buf << "><v>" << xml_val.to_s << "</v></c>" end col_index += 1 end buf << "</row>" return unless buf.bytesize >= 32_768 @io.write(buf) buf.clear end
Highly optimized row writing for StreamWriter that avoids allocating intermediate Hashes.